package main

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
)

var postedMap sync.Map

type PostInfo struct {
	ID         string
	Title      string
	PicURL     string
	Category   string
	Price      string
	Worthy     int64
	Unworthy   int64
	Collection int64
	Comment    int64
	Content    string
	URL        string
	TimeSort   int64
	RMBPrice   int64
}

func postToWeChat(info *PostInfo) {
	type wechatArticle struct {
		Title  string `json:"title"`
		Desc   string `json:"description"`
		URL    string `json:"url"`
		PicURL string `json:"picurl"`
	}

	type wechatNews struct {
		Articles []wechatArticle `json:"articles"`
	}

	type wechatJSON struct {
		MsgType string     `json:"msgtype"`
		News    wechatNews `json:"news"`
	}

	if j, err1 := json.Marshal(wechatJSON{
		MsgType: "news",
		News: wechatNews{
			Articles: []wechatArticle{
				wechatArticle{
					Title:  info.Title,
					Desc:   info.Content,
					URL:    info.URL,
					PicURL: info.PicURL,
				},
			},
		},
	}); nil != err1 {
		log.Printf("postToWeChat Marshal err: %s\n", err1.Error())
	} else {
		buf := bytes.NewBuffer(make([]byte, 0))
		buf.Write(j)

		url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s", cfg.WechatKey)
		if req, err2 := http.NewRequest("POST", url, buf); nil != err2 {
			log.Printf("postToWeChat NewRequest %s err: %s\n", url, err2.Error())
		} else {
			tr := &http.Transport{
				TLSClientConfig: &tls.Config{
					InsecureSkipVerify: true,
				},
			}

			client := http.Client{
				Transport: tr,
				Timeout:   cfg.HTTPTimeout,
			}

			req.Header.Set("Content-Type", "application/json; charset=UTF-8")

			if res, err3 := client.Do(req); nil != err3 {
				log.Printf("postToWeChat Do %s err: %s\n", url, err3.Error())
			} else if http.StatusOK != res.StatusCode {
				log.Printf("postToWeChat Do %s errCode: %d\n", url, res.StatusCode)
			} else {
				// log.Printf("%s\t%s\t%s\n", info.Title, info.Price, info.URL)
			}
		}
	}
}

func postToWxPusher(info *PostInfo) {
	type wxPusherJSON struct {
		AppToken    string   `json:"appToken"`
		Content     string   `json:"content"`
		Summary     string   `json:"summary"`
		ContentType int64    `json:"contentType"` // 1 - 文字，2 - HTML，3 - MARKDOWN
		TopicIDs    []int64  `json:"topicIds"`
		UIDs        []string `json:"uids"`
		URL         string   `json:"url"`
	}

	content := fmt.Sprintf(
		"<img src=\"http%s\" style=\"display: block; margin: 0 auto\" />"+
			"<h2>%s</h2>"+
			"<p>分类：%s</p>"+
			"<p><font color=\"#DD0000\">价格：%s</font></p>"+
			"<p>点值：%d，点不值：%d</p>"+
			"<p>收藏数：%d，评论数：%d</p>"+
			"<p>爆料内容：%s</p>"+
			"<p>购买链接：<a href=\"%s\">%s</a></p>",
		info.PicURL[5:len(info.PicURL)-9],
		info.Title,
		info.Category,
		info.Price,
		info.Worthy, info.Unworthy,
		info.Collection, info.Comment,
		info.Content,
		info.URL, info.URL)

	if j, err1 := json.Marshal(wxPusherJSON{
		AppToken:    cfg.WxPusherAppToken,
		Content:     content,
		Summary:     info.Title + "\n" + info.Price,
		ContentType: 2,
		TopicIDs:    cfg.WxPusherTopicIDs,
		UIDs:        cfg.WxPusherUIDs,
		URL:         info.URL,
	}); nil != err1 {
		log.Printf("postToWxPusher Marshal err: %s\n", err1.Error())
	} else {
		buf := bytes.NewBuffer(make([]byte, 0))
		buf.Write(j)

		url := "http://wxpusher.zjiecode.com/api/send/message"
		if req, err2 := http.NewRequest("POST", url, buf); nil != err2 {
			log.Printf("postToWxPusher NewRequest %s err: %s\n", url, err2.Error())
		} else {
			tr := &http.Transport{
				TLSClientConfig: &tls.Config{
					InsecureSkipVerify: true,
				},
			}

			client := http.Client{
				Transport: tr,
				Timeout:   cfg.HTTPTimeout,
			}

			req.Header.Set("Content-Type", "application/json; charset=UTF-8")

			if res, err3 := client.Do(req); nil != err3 {
				log.Printf("postToWxPusher Do %s err: %s\n", url, err3.Error())
			} else if http.StatusOK != res.StatusCode {
				log.Printf("postToWxPusher Do %s errCode: %d\n", url, res.StatusCode)
			} else {
				// log.Printf("%s\t%s\t%s\n", info.Title, info.Price, info.URL)
			}
		}
	}
}

func postToServerChan(info *PostInfo) {
	params := url.Values{}
	params.Add("text", info.Title)
	params.Add("desp", fmt.Sprintf(
		"![pic](http%s)"+
			"## %s<br>\r\n"+
			"## 分类：%s<br>\r\n"+
			"## 价格：%s<br>\r\n"+
			"## 点值：%d，点不值：%d<br>\r\n"+
			"## 收藏数：%d，评论数：%d<br>\r\n"+
			"## 爆料内容：%s<br>\r\n"+
			"## 购买链接：[%s](%s)<br>\r\n",
		info.PicURL[5:],
		info.Title,
		info.Category,
		info.Price,
		info.Worthy, info.Unworthy,
		info.Collection, info.Comment,
		info.Content,
		info.URL, info.URL))

	buf := strings.NewReader(params.Encode())
	url := fmt.Sprintf("https://sc.ftqq.com/%s.send", cfg.ServerChanSKey)
	if req, err1 := http.NewRequest("POST", url, buf); nil != err1 {
		log.Printf("postToServerChan NewRequest %s err: %s\n", url, err1.Error())
	} else {
		tr := &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
		}

		client := http.Client{
			Transport: tr,
			Timeout:   cfg.HTTPTimeout,
		}

		req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

		if res, err2 := client.Do(req); nil != err2 {
			log.Printf("postToServerChan Do %s err: %s\n", url, err2.Error())
		} else if http.StatusOK != res.StatusCode {
			log.Printf("postToServerChan Do %s errCode: %d\n", url, res.StatusCode)
		} else {
			// log.Printf("%s\t%s\t%s\n", info.Title, info.Price, info.URL)
		}
	}
}

func filter(list []PostInfo) int64 {
	var maxTime int64
	for _, item := range list {
		if maxTime < item.TimeSort {
			maxTime = item.TimeSort
		}

		// log.Println(item.Title)

		// 过滤已推送的爆料
		if _, ok := postedMap.Load(item.ID); ok {
			continue
		}

		// 过滤标题
		isMatch := false
		for _, kw := range cfg.TitleExcept {
			if strings.Contains(item.Title, kw) {
				isMatch = true
				break
			}
		}

		if isMatch {
			continue
		}

		// 免过滤的标题
		isMatch = false
		for _, kw := range cfg.TitlePass {
			if strings.Contains(item.Title, kw) {
				isMatch = true
				break
			}
		}

		if !isMatch {
			// worthies := item.Worthy + item.Unworthy
			if !(cfg.WorthyMin > 0 && item.Worthy >= cfg.WorthyMin) &&
				!(cfg.CollectionMin > 0 && item.Collection >= cfg.CollectionMin) &&
				!(cfg.CommentMin > 0 && item.Comment >= cfg.CommentMin) {
				continue
			}

			// 价格过高
			if cfg.RMBMax > 0 && item.RMBPrice > cfg.RMBMax {
				continue
			}

			// 过滤品类
			isMatch = false
			for _, kw := range cfg.CategoryExcept {
				if strings.Contains(item.Category, kw) {
					isMatch = true
					break
				}
			}

			if isMatch {
				continue
			}
		}

		postedMap.Store(item.ID, 1)
		log.Printf("%s\t值：%d\t收藏：%d\t评论：%d\t%s\t%s\n", item.Title, item.Worthy, item.Collection, item.Comment, item.Price, item.URL)

		// postToWeChat(&item)
		postToWxPusher(&item)

		// 匹配了免过滤标题，抄送到 ServerChan
		if isMatch {
			postToServerChan(&item)
		}
	}

	return maxTime
}

func crawleFaXian(tm int64) {
	type faXianGTM struct {
		RMBPrice int64  `json:"rmb_price"`
		Category string `json:"cates_str"`
	}

	type faXianItem struct {
		ArticleID         int         `json:"article_id"`
		ArticleURL        string      `json:"article_url"`
		ArticlePicURL     string      `json:"article_pic_url"`
		ArticleTitle      string      `json:"article_title"`
		ArticlePrice      string      `json:"article_price"`
		ArticleContent    string      `json:"article_content"`
		ArticleWorthy     int64       `json:"article_rating"`
		ArticleCollection interface{} `json:"article_collection"`
		ArticleComment    int64       `json:"article_comment"`
		ArticleTimeSort   int64       `json:"timesort"`
		GTM               faXianGTM   `json:"gtm"`
	}

	var interval time.Duration = cfg.CrawleInterval
	defer time.AfterFunc(interval, func() {
		crawleFaXian(tm)
	})

	url := fmt.Sprintf("https://faxian.smzdm.com/json_more?type=a&timesort=%d", tm)
	if req, err1 := http.NewRequest("GET", url, nil); nil != err1 {
		log.Printf("crawleFaXian NewRequest %s err: %s\n", url, err1.Error())
	} else {
		tr := &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
		}

		client := http.Client{
			Transport: tr,
			Timeout:   cfg.HTTPTimeout,
		}

		if res, err2 := client.Do(req); nil != err2 {
			log.Printf("crawleFaXian Do %s err: %s\n", url, err2.Error())
		} else if http.StatusOK != res.StatusCode {
			log.Printf("crawleFaXian Do %s errCode: %d\n", url, res.StatusCode)
		} else {
			if ret, err3 := ioutil.ReadAll(res.Body); nil != err3 {
				log.Printf("crawleFaXian ReadAll %s err: %s\n", url, err3.Error())
			} else {
				var j []faXianItem
				if err4 := json.Unmarshal(ret, &j); nil != err4 {
					log.Printf("crawleFaXian Unmarshal\n%s\nerr: %s\n", string(ret), err4.Error())
				} else {
					var list []PostInfo = make([]PostInfo, 0)
					for _, item := range j {
						var collection int64
						if collection1, ok1 := item.ArticleCollection.(string); ok1 {
							collection, _ = strconv.ParseInt(collection1, 10, 64)
						} else if collection2, ok2 := item.ArticleCollection.(int64); ok2 {
							collection = collection2
						}

						list = append(list, PostInfo{
							ID:         strconv.Itoa(item.ArticleID),
							Title:      item.ArticleTitle,
							PicURL:     item.ArticlePicURL,
							Category:   item.GTM.Category,
							Price:      item.ArticlePrice,
							Worthy:     item.ArticleWorthy,
							Collection: collection,
							Comment:    item.ArticleComment,
							Content:    item.ArticleContent,
							URL:        item.ArticleURL,
							TimeSort:   item.ArticleTimeSort,
							RMBPrice:   item.GTM.RMBPrice,
						})
					}

					if tim := filter(list); 0 != tim {
						if time.Now().Sub(time.Unix(tim, 0)) < cfg.CrawleRecentlyMax {
							tm = tim
						} else {
							tm = time.Now().Unix()
							interval = cfg.CrawleNextInterval
						}
					}
				}
			}
		}
	}
}

func crawleJingXuan(tm int64) {
	type jingXuanGTM struct {
		RMBPrice int64  `json:"rmb_price"`
		Category string `json:"cates_str"`
	}

	type jingXuanItem struct {
		ArticleID         int         `json:"article_id"`
		ArticleURL        string      `json:"article_url"`
		ArticlePicURL     string      `json:"article_pic_url"`
		ArticleTitle      string      `json:"article_title"`
		ArticlePrice      string      `json:"article_price"`
		ArticleContent    string      `json:"article_content"`
		ArticleWorthy     int64       `json:"article_worthy"`
		ArticleUnworthy   int64       `json:"article_unworthy"`
		ArticleCollection int64       `json:"article_collection"`
		ArticleComment    int64       `json:"article_comment"`
		ArticleTimeSort   int64       `json:"article_timesort"`
		GTM               jingXuanGTM `json:"gtm"`
	}

	type jingXuanJSON struct {
		ArticleTotal int64          `json:"article_total"`
		ArticleList  []jingXuanItem `json:"article_list"`
	}

	var interval time.Duration = cfg.CrawleInterval
	defer time.AfterFunc(interval, func() {
		crawleJingXuan(tm)
	})

	url := fmt.Sprintf("https://www.smzdm.com/jingxuan/json_more?filter=s0f0t0b0d0r0p0&timesort=%d", tm)
	if req, err1 := http.NewRequest("GET", url, nil); nil != err1 {
		log.Printf("crawleJingXuan NewRequest %s err: %s\n", url, err1.Error())
	} else {
		tr := &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
		}

		client := http.Client{
			Transport: tr,
			Timeout:   cfg.HTTPTimeout,
		}

		if res, err2 := client.Do(req); nil != err2 {
			log.Printf("crawleJingXuan Do %s err: %s\n", url, err2.Error())
		} else if http.StatusOK != res.StatusCode {
			log.Printf("crawleJingXuan Do %s errCode: %d\n", url, res.StatusCode)
		} else {
			if ret, err3 := ioutil.ReadAll(res.Body); nil != err3 {
				log.Printf("crawleJingXuan ReadAll %s err: %s\n", url, err3.Error())
			} else {
				var j jingXuanJSON
				if err4 := json.Unmarshal(ret, &j); nil != err4 {
					log.Printf("crawleJingXuan Unmarshal\n%s\nerr: %s\n", string(ret), err4.Error())
				} else {
					var list []PostInfo = make([]PostInfo, 0)
					for _, item := range j.ArticleList {
						list = append(list, PostInfo{
							ID:         strconv.Itoa(item.ArticleID),
							Title:      item.ArticleTitle,
							PicURL:     item.ArticlePicURL,
							Category:   item.GTM.Category,
							Price:      item.ArticlePrice,
							Worthy:     item.ArticleWorthy,
							Unworthy:   item.ArticleUnworthy,
							Collection: item.ArticleCollection,
							Comment:    item.ArticleComment,
							Content:    item.ArticleContent,
							URL:        item.ArticleURL,
							TimeSort:   item.ArticleTimeSort,
							RMBPrice:   item.GTM.RMBPrice,
						})
					}

					if tim := filter(list); 0 != tim {
						if time.Now().Sub(time.Unix(tim, 0)) < cfg.CrawleRecentlyMax {
							tm = tim
						} else {
							tm = time.Now().Unix()
							interval = cfg.CrawleNextInterval
						}
					}
				}
			}
		}
	}
}

func main() {
	initConfig()

	go crawleFaXian(time.Now().Unix())
	go crawleJingXuan(time.Now().Unix())

	for {
		<-time.After(cfg.CrawleInterval)
	}
}
